// 服务端业务处理模块
#ifndef __SERVICE_H__
#define __SERVICE_H__
#include "hot.hpp"
#include "httplib.h"

extern cloud::DataManager *_data; // 因为也会访问数据管理类
namespace cloud
{
    class Service
    {
    private:
        int _server_port;             // 服务器端口号
        std::string _server_ip;       // 服务器ip地址
        std::string _download_prefix; // 下载请求前缀，带有这个前缀就认为是文件下载请求
        // 上面三个都可以从配置信息中拿
        // 搭建服务器使用的是httplib库-实例化一个server对象，通过上面三个信息搭建一个服务器
        httplib::Server _server;

    public:
        // 构造函数完成各个成员变量的初始化
        Service()
        {
            //_server直接实例化完毕
            cloud::Config *config = cloud::Config::GetInstance();
            _server_port = config->GetServerPort();
            _server_ip = config->GetServerIp();
            _download_prefix = config->GetDownloadPrefix();
            // 当文件的备份目录不存在要进行创建
            std::string back_dir = config->GetBackDir();
            FileUtil fu(back_dir);
            if (fu.Exists() == false)
            {
                fu.CreateDirectory();
            }
        }
        // 运行模块
        bool RunModule()
        {
            // Post请求的upload表示文件资源的上传，这里的处理函数必须是静态的
            _server.Post("/upload", Upload);
            // 文件列表查看请求
            _server.Get("/listshow", ListShow);
            _server.Get("/", ListShow);
            // 文件下载请求
            // 正则表达式 .*表示匹配任意一个字符任意次
            std::string download_url = _download_prefix + "(.*)";
            _server.Get(download_url, DownLoad);
            // 监听
            _server.listen(_server_ip.c_str(), _server_port);
            return true;
        }

    private:
        // 业务处理请求-文件上传请求、展示页面获取请求以及文件下载请求
        // 业务上传请求处理
        // 同时要将配置文件中的ip地址改为0.0.0.0否则无法监听!!!!!!!
        static void Upload(const httplib::Request &req, httplib::Response &rsp)
        {
            // 文件上传是一个Post请求的upload才可以
            // 判断有没有这个上传的文件区域
            auto ret = req.has_file("file");
            if (ret == false)
            {
                rsp.status = 400; // 设置状态码为400
                return;
            }
            // 如果有的话就获取文件数据(文件名，文件数据等)，file是MultipartFormData类型的数据
            const auto &file = req.get_file_value("file");
            // 然后需要在备份目录下创建这个文件并把数据写进去
            std::string back_dir = Config::GetInstance()->GetBackDir(); // 获取备份目录
            // 获取文件的实际存储路径
            std::string realpath = back_dir + FileUtil(file.filename).FileName();
            FileUtil fu(realpath);
            // 将文件中的正文数据放入文件（文件的数据内容被httplib从你上传的文件中解析出来）
            fu.SetContent(file.content);
            // 组织备份的文件信息
            BackupInfo info;
            info.NewBackupInfo(realpath);
            // 向数据管理模块添加备份的文件信息
            _data->Insert(info);
            return;
        }

        // 实现一个接口将时间戳转换为字符串
        static std::string TimetoString(time_t t)
        {
            return std::ctime(&t);
        }

        //  获取页面展示请求的业务处理函数
        static void ListShow(const httplib::Request &req, httplib::Response &rsp)
        {
            // 将时间戳转换成时间格式
            //  1、获取所有的文件备份信息
            //  服务器一运行就从原来备份的持久化存储文件中加载数据
            std::vector<BackupInfo> array;
            // 获取到所有的数据信息
            _data->GetAll(&array);
            // 2、根据备份信息组织成一个html文件数据
            std::stringstream ss; // 使用字符串流
            ss << "<html><meta charset = 'utf-8'><head><title>DownLoad</title></head>";
            ss << "<body><h1>DownLoad</h1><table>";
            for (auto &a : array)
            {
                // 根据每一个文件组织中间的数据
                ss << "<tr>";
                // url链接随着资源的不同而不同
                // html中不区分单引号和双引号，所以改成单引号
                std::string filename = FileUtil(a.real_path).FileName(); // 只要文件名
                ss << "<td> <a href = '" << a.url << "'>" << filename << "</a></td>";
                // 要将时间戳转换为时间格式
                ss << "<td align = 'right'>" << TimetoString(a.atime) << "</td>";
                ss << "<td align = 'right'>" << a.fsize / 1024 << "K</td>"; // 文件大小，以K为单位
                ss << "</tr>";
            }
            ss << "</table></body></html>"; // 结尾
            rsp.body = ss.str();
            // 设置头部信息,正文内容是html类型文本
            rsp.set_header("Content-Type", "text/html");
            rsp.status = 200;
            return;
        }

        // 先生成ETag判断文件是否修改-根据文件名-文件大小-文件最后一次修改时间组成
        static std::string GetETag(const BackupInfo &info)
        {
            FileUtil fu(info.real_path);
            std::string ETag = fu.FileName();
            ETag += "-";
            ETag += std::to_string(info.fsize);
            ETag += "-";
            ETag += std::to_string(info.mtime);
            return ETag;
        }

        // 文件下载请求的处理函数
        static void DownLoad(const httplib::Request &req, httplib::Response &rsp)
        {
            // 1、获取客户端请求的资源路径path，其实就是req里面的path
            //  2、根据req资源路径，获取文件备份信息
            BackupInfo info;
            _data->GetOneByUrl(req.path, &info);
            // 3、判断文件是否被压缩，如果被压缩要先解压
            if (info.pack_flag == true)
            {
                FileUtil fu(info.pack_path);
                fu.UnCompress(info.real_path);
                // 删除压缩包，修改文件信息
                info.pack_flag = false;
                fu.Remove();
                // 更新
                _data->Update(info);
            }
            // 4、读取文件数据，放入rsp.body中
            FileUtil fu(info.real_path);
            bool retrans = false; // 标记当前是否是断点续传
            std::string old_etag; // 下载时已有的ETag的值
            // 判断是否存在If-Range头部字段，如果有就是断点续传，如果没有就不是
            if (req.has_header("If-Range"))
            {
                old_etag = req.get_header_value("If-Range");
                // 如果没有If-Range字段则是正常下载，或者如果有这个字段，但是与当前etag不一致
                // 也必须重新下载
                // 有If-Range字段且这个字段的值与请求文件的最新的etag一致则符合断点续传
                if (old_etag == GetETag(info))
                {
                    retrans = true;
                }
            }
            // 如果retrans == false 则是文件的正常下载
            if (retrans == false)
            {
                fu.GetContent(&rsp.body);
                // 5、设置响应头部字段ETag
                //  5、设置响应头部字段ETag,Accept-Ranges:bytes
                rsp.set_header("Accept-Ranges", "bytes");
                rsp.set_header("ETag", GetETag(info));
                // 设置头部字段，表示如何处理数据，这里表示想要的是二进制流，常用于文件下载
                rsp.set_header("Content-Type", "application/octet-stream");
                rsp.status = 200;
            }
            else
            {
                // 此时表示是一个断点续传，取出Range字段了解请求区间的范围然后去读取指定区间的数据
                // httplib中已经进行了解析，我们要做的就是读取文件到rsp.body中
                fu.GetContent(&rsp.body);
                rsp.set_header("Accept-Ranges", "bytes");
                rsp.set_header("ETag", GetETag(info));
                // 设置头部字段，表示如何处理数据，这里表示想要的是二进制流，常用于文件下载
                rsp.set_header("Content-Type", "application/octet-stream");
                rsp.status = 206; // 其实我们不用给，httplib里都已经操作了
            }
        }
    };
}

#endif
